/*
 * Copyright (c) 2012 swing-on-fire Team
 * 
 * This file is part of Swing-On-Fire (http://code.google.com/p/swing-on-fire), licensed under the terms of the MIT
 * License (MIT).
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.google.code.swingonfire.builder;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.LayoutManager;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventListener;
import java.util.List;

import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.border.Border;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionListener;

public abstract class AbstractJComponentBuilder<SELF, TYPE extends JComponent>
{

    protected class AddAction
    {
        private final Component comp;
        private final Integer index;
        private final Object constraints;

        public AddAction(Component comp, Integer index, Object constraints)
        {
            super();

            this.comp = comp;
            this.index = index;
            this.constraints = constraints;
        }

        public void perform(JComponent component)
        {
            if ((index != null) && (constraints != null))
            {
                component.add(comp, constraints, index);
            }
            else if (index != null)
            {
                component.add(comp, index);
            }
            else if (constraints != null)
            {
                component.add(comp, constraints);
            }
            else
            {
                component.add(comp);
            }
        }
    }

    private final Class<TYPE> type;

    private List<AddAction> addActions;
    private Boolean enabled;
    private Boolean visible;
    private String text;
    private Icon icon;
    private Font font;
    private String fontName;
    private Integer fontStyle;
    private Float fontSize;
    private Float fontScale;
    private LayoutManager layout;
    private Border border;
    private Color background;
    private Color foreground;
    private Boolean opaque;
    private Dimension preferredSize;
    private Dimension minimumSize;
    private Dimension maximumSize;
    private String actionCommand;
    private List<EventListener> listeners;

    public AbstractJComponentBuilder(Class<TYPE> type)
    {
        super();

        this.type = type;
    }

    public SELF add(Component comp)
    {
        return addAction(new AddAction(comp, null, null));
    }

    public SELF add(Component comp, Object constraints)
    {
        return addAction(new AddAction(comp, null, constraints));
    }

    public SELF add(Component comp, int index)
    {
        return addAction(new AddAction(comp, Integer.valueOf(index), null));
    }

    public SELF add(Component comp, int index, Object constraints)
    {
        return addAction(new AddAction(comp, index, constraints));
    }

    @SuppressWarnings("unchecked")
    protected SELF addAction(AddAction addAction)
    {
        if (addActions == null)
        {
            addActions = new ArrayList<AbstractJComponentBuilder<SELF, TYPE>.AddAction>();
        }

        addActions.add(addAction);

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF enabled(boolean enabled)
    {
        this.enabled = Boolean.valueOf(enabled);

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF visible(boolean visible)
    {
        this.visible = Boolean.valueOf(visible);

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF text(String text)
    {
        this.text = text;

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF icon(Icon icon)
    {
        this.icon = icon;

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF font(Font font)
    {
        this.font = font;

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF font(String fontName)
    {
        this.fontName = fontName;

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF fontStyle(int fontStyle)
    {
        this.fontStyle = Integer.valueOf(fontStyle);

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF fontSize(double fontSize)
    {
        this.fontSize = Float.valueOf((float) fontSize);

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF fontScale(double fontScale)
    {
        this.fontScale = Float.valueOf((float) fontScale);

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF layout(LayoutManager layout)
    {
        this.layout = layout;

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF border(Border border)
    {
        this.border = border;

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF background(Color background)
    {
        this.background = background;

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF foreground(Color foreground)
    {
        this.foreground = foreground;

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF opaque(boolean opaque)
    {
        this.opaque = opaque;

        return (SELF) this;
    }

    @SuppressWarnings("unchecked")
    public SELF preferredSize(Dimension preferredSize)
    {
        this.preferredSize = preferredSize;

        return (SELF) this;
    }

    public SELF preferredSize(int width, int height)
    {
        return preferredSize(new Dimension(width, height));
    }

    @SuppressWarnings("unchecked")
    public SELF minimumSize(Dimension minimumSize)
    {
        this.minimumSize = minimumSize;

        return (SELF) this;
    }

    public SELF minimumSize(int width, int height)
    {
        return minimumSize(new Dimension(width, height));
    }

    @SuppressWarnings("unchecked")
    public SELF maximumSize(Dimension maximumSize)
    {
        this.maximumSize = maximumSize;

        return (SELF) this;
    }

    public SELF maximumSize(int width, int height)
    {
        return maximumSize(new Dimension(width, height));
    }

    @SuppressWarnings("unchecked")
    public SELF actionCommand(String actionCommand)
    {
        this.actionCommand = actionCommand;

        return (SELF) this;
    }

    public SELF listener(String actionCommand, EventListener... listeners)
    {
        actionCommand(actionCommand);

        return listener(listeners);
    }

    @SuppressWarnings("unchecked")
    public SELF listener(EventListener... listeners)
    {
        if (this.listeners == null)
        {
            this.listeners = new ArrayList<EventListener>();
        }

        Collections.addAll(this.listeners, listeners);

        return (SELF) this;
    }

    public TYPE build()
    {
        TYPE component;

        try
        {
            component = newInstance(type);
        }
        catch (InstantiationException e)
        {
            throw new IllegalArgumentException("Failed to instantiate " + type, e);
        }
        catch (IllegalAccessException e)
        {
            throw new IllegalArgumentException("Failed to access " + type, e);
        }
        catch (IllegalArgumentException e)
        {
            throw new IllegalArgumentException("Failed to instantiate " + type, e);
        }
        catch (InvocationTargetException e)
        {
            throw new IllegalArgumentException("Failed to instantiate " + type, e);
        }
        catch (NoSuchMethodException e)
        {
            throw new IllegalArgumentException("Failed to instantiate " + type, e);
        }
        catch (SecurityException e)
        {
            throw new IllegalArgumentException("Failed to access " + type, e);
        }

        populate(component);

        return component;
    }

    protected TYPE newInstance(Class<TYPE> type) throws InstantiationException, IllegalAccessException,
        IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException
    {
        return type.newInstance();
    }

    protected void populate(TYPE component)
    {
        if (layout != null)
        {
            component.setLayout(layout);
        }

        if (addActions != null)
        {
            for (AddAction add : addActions)
            {
                add.perform(component);
            }
        }

        if (enabled != null)
        {
            component.setEnabled(enabled.booleanValue());
        }

        if (visible != null)
        {
            component.setVisible(visible.booleanValue());
        }

        if (text != null)
        {
            set(component, "setText", text);
        }

        if (icon != null)
        {
            set(component, "setIcon", icon);
        }

        if (font != null)
        {
            component.setFont(font);
        }

        if (fontName != null)
        {
            component.setFont(Font.decode(fontName));
        }

        if (fontStyle != null)
        {
            component.setFont(component.getFont().deriveFont(fontStyle.intValue()));
        }

        if (fontSize != null)
        {
            component.setFont(component.getFont().deriveFont(fontSize.floatValue()));
        }

        if (fontScale != null)
        {
            Font font = component.getFont();

            component.setFont(font.deriveFont(font.getSize() * fontScale.floatValue()));
        }

        if (border != null)
        {
            component.setBorder(border);
        }

        if (background != null)
        {
            component.setBackground(background);
        }

        if (foreground != null)
        {
            component.setForeground(foreground);
        }

        if (opaque != null)
        {
            component.setOpaque(opaque);
        }

        if (preferredSize != null)
        {
            component.setPreferredSize(preferredSize);
        }

        if (minimumSize != null)
        {
            component.setPreferredSize(minimumSize);
        }

        if (maximumSize != null)
        {
            component.setPreferredSize(maximumSize);
        }

        if (actionCommand != null)
        {
            set(component, "setActionCommand", actionCommand);
        }

        if (listeners != null)
        {
            for (EventListener listener : listeners)
            {
                setListener(component, listener);
            }
        }
    }

    protected void setListener(TYPE component, EventListener listener)
    {
        if (listener instanceof ActionListener)
        {
            set(component, "addActionListener", listener);
        }
        else if (listener instanceof ChangeListener)
        {
            set(component, "addChangeListener", listener);
        }
        else if (listener instanceof ListSelectionListener)
        {
            set(component, "addListSelectionListener", listener);
        }
    }

    protected Object set(JComponent component, String setterMethod, Object value)
    {
        Class<? extends JComponent> type = component.getClass();
        Class<? extends Object> parameterType = value.getClass();
        Method method = findMethod(type, setterMethod, parameterType);

        if (method == null)
        {
            for (Class<? extends Object> iface : parameterType.getInterfaces())
            {
                method = findMethod(type, setterMethod, iface);

                if (method != null)
                {
                    break;
                }
            }
        }

        if (method == null)
        {
            parameterType = parameterType.getSuperclass();

            while ((method == null) && (parameterType != null))
            {
                method = findMethod(type, setterMethod, parameterType);

                parameterType = parameterType.getSuperclass();
            }
        }

        if (method == null)
        {
            throw new UnsupportedOperationException(setterMethod + " not supported by " + type);
        }

        try
        {
            return method.invoke(component, value);
        }
        catch (IllegalAccessException e)
        {
            throw new IllegalArgumentException("Failed to access " + method, e);
        }
        catch (IllegalArgumentException e)
        {
            throw new IllegalArgumentException("Failed to invoke " + method, e);
        }
        catch (InvocationTargetException e)
        {
            throw new IllegalArgumentException("Failed to invoke " + method, e);
        }
    }

    private Method findMethod(Class<? extends JComponent> type, String setterMethod,
        Class<? extends Object> parameterType)
    {
        try
        {
            return type.getMethod(setterMethod, parameterType);
        }
        catch (NoSuchMethodException e)
        {
            return null;
        }
        catch (SecurityException e)
        {
            throw new IllegalArgumentException("Failed to access " + setterMethod + " of " + type, e);
        }
    }

}
